package org.example;

import java.awt.image.BufferedImage;
import java.io.*;
import java.net.URL;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.stream.IntStream;

import org.apache.commons.imaging.Imaging;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;

import javax.imageio.ImageIO;

/**
 * 天地图数据爬取
 * @author RainingTime
 * @date 2021.09.17
 */
public class TianDiTuDownload {

    //矢量（行政） - 等经纬度
    public static String vec_c = "http://{server}.tianditu.gov.cn/vec_c/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=vec&STYLE=default&TILEMATRIXSET=c&FORMAT=tiles&TILEMATRIX={z}&TILECOL={x}&TILEROW={y}&tk={tk}";
    //矢量（行政） - 墨卡托
    public static String vec_w = "http://{server}.tianditu.gov.cn/vec_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=vec&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&tk={tk}&TILECOL={x}&TILEROW={y}&TILEMATRIX={z}";
    //矢量注记（行政） - 等经纬度
    public static String cva_c = "http://{server}.tianditu.gov.cn/cva_c/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=cva&STYLE=default&TILEMATRIXSET=c&FORMAT=tiles&tk={tk}&TILECOL={x}&TILEROW={y}&TILEMATRIX={z}";
    //矢量注记（行政） - 墨卡托
    public static String cva_w = "http://{server}.tianditu.gov.cn/cva_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=cva&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&tk={tk}&TILECOL={x}&TILEROW={y}&TILEMATRIX={z}";

    //影像 - 等经纬度
    public static String img_c = "http://{server}.tianditu.gov.cn/img_c/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=img&STYLE=default&TILEMATRIXSET=c&FORMAT=tiles&tk={tk}&TILECOL={x}&TILEROW={y}&TILEMATRIX={z}";
    //影像 - 墨卡托
    public static String img_w = "http://{server}.tianditu.gov.cn/img_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=img&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&tk={tk}&TILECOL={x}&TILEROW={y}&TILEMATRIX={z}";
    //影像注记 - 等经纬度
    public static String cia_c = "http://{server}.tianditu.gov.cn/cia_c/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=cia&STYLE=default&TILEMATRIXSET=c&FORMAT=tiles&tk={tk}&TILECOL={x}&TILEROW={y}&TILEMATRIX={z}";
    //影像注记 - 墨卡托
    public static String cia_w = "http://{server}.tianditu.gov.cn/cia_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=cia&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&tk={tk}&TILECOL={x}&TILEROW={y}&TILEMATRIX={z}";

    //地形 - 等经纬度
    public static String ter_c = "http://{server}.tianditu.gov.cn/ter_c/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=ter&STYLE=default&TILEMATRIXSET=c&FORMAT=tiles&tk={tk}TILEMATRIX={z}&TILECOL={x}&TILEROW={y}&TILEMATRIX={z}";
    //地形 - 墨卡托
    public static String ter_w = "http://{server}.tianditu.gov.cn/ter_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=ter&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&tk={tk}TILEMATRIX={z}&TILECOL={x}&TILEROW={y}&TILEMATRIX={z}";
    //地形注记 - 等经纬度
    public static String cta_c = "http://{server}.tianditu.gov.cn/cta_c/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=cta&STYLE=default&TILEMATRIXSET=c&FORMAT=tiles&tk={tk}TILEMATRIX={z}&TILECOL={x}&TILEROW={y}&TILEMATRIX={z}";
    //地形注记 - 墨卡托
    public static String cta_w = "http://{server}.tianditu.gov.cn/cta_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=cta&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&tk={tk}&TILECOL={x}&TILEROW={y}&TILEMATRIX={z}";

    public static String[] servers = {"t0", "t1", "t2", "t3", "t4", "t5", "t6", "t7"};


    private static final long SLEEP_TIME = 1000L;  // 线程休息时间，单位：毫秒
    private final static int BAD_IMAGE_SIZE = 1024; // 无效图片大小, 单位：字节 Bits

    private final static int MAX_LOOP_TIME = 3; // 地图数据获取最大试错次数，因为个人最多可以获取5个Key，这里设置5

    // 保存一系列的天地图开发者秘钥
    private static final CopyOnWriteArrayList<String> tkList = new CopyOnWriteArrayList<>();

    /*
     * startLat: 53.58°N（纬度的最小值）
     * endLat: 2.7°N（纬度的最大值）
     * startLon: 73.2°E（经度的最小值）
     * endLon: 135.15°E（经度的最大值）
     * 这个区域大致覆盖了从东经73.2度到东经135.15度，北纬53.58度到北纬2.7度的区域。这个区域包括了亚洲的部分地区，特别是中国西部、中亚和东南亚的部分地区。
     *
     * 为了更具体地确定这个区域，我们可以查看这些坐标大致对应的地理位置：
     *
     * 北纬53.58度：大约是中国黑龙江省北部的纬度。
     * 北纬2.7度：大约是印度尼西亚苏门答腊岛的纬度。
     * 东经73.2度：大约是中国新疆维吾尔自治区的经度。
     * 东经135.15度：大约是日本本州岛西部的经度。
     * 因此，这个区域大致覆盖了中国西部、中亚和东南亚的部分地区，包括中国、哈萨克斯坦、吉尔吉斯斯坦、塔吉克斯坦、阿富汗、巴基斯坦、印度、尼泊尔、不丹、孟加拉国、缅甸、老挝、泰国、柬埔寨、越南、马来西亚、新加坡、印度尼西亚和菲律宾等国家的部分或全部地区。
     */
    public static final double startLatChina = 53.58;//开始纬度（从北到南）
    public static final double endLatChina = 2.7;//结束纬度（从北到南）
    public static final double startLonChina = 73.2;//开始经度（从西到东）
    public static final double endLonChina = 135.15;//结束经度（从西到东）

    public static void main(String[] args){
        String basePath = "C:/Users/兰/Desktop/天地图/map-download";

        //这里放你的天地图开发者秘钥，注意天地图API访问次数限制
        String[] tk = {"3a3989a46fc37c0a0e9c166fd8f6bc83", "0b93af57c88f73c64282fc56fd82fce1", "504749bb489302cc97b9caa3dd0fddb0"};

        // 初始化秘钥
        tkList.addAll(Arrays.asList(tk));

//        String[] urlArr = {vec_c, vec_w, cva_c, cva_w, img_c, img_w, cia_c, cia_w, ter_c, ter_w, cta_c, cta_w};//要下载的图层
        String[] urlArr = {vec_w, cva_w};

        int minZoom = 1;
        int maxZoom = 7;
//        int maxZoom = 21;
        // 中国范围
        final double startLatf = startLatChina;//开始纬度（从北到南）
        final double endLatf = endLatChina;//结束纬度（从北到南）
        final double startLonf = startLonChina;//开始经度（从西到东）
        final double endLonf = endLonChina;//结束经度（从西到东）

        // 这里定义湖北省范围
//        final double startLatf = 33.11;//开始纬度（从北到南）
//        final double endLatf = 29.03;//结束纬度（从北到南）
//        final double startLonf = 108.36;//开始经度（从西到东）
//        final double endLonf = 116.13;//结束经度（从西到东）

        // 这里定义湖北省宜昌市范围
//        final double startLatf = 31.5667;//开始纬度（从北到南）
//        final double endLatf = 29.9333;//结束纬度（从北到南）
//        final double startLonf = 110.25;//开始经度（从西到东）
//        final double endLonf = 112.0667;//结束经度（从西到东）


        //等经纬度第一层是1x2，纬度数量是2^0，经度数量是2^1
        //墨卡托投影第一层是2x2，纬度数量是2^1，经度数量是2^1
        Arrays.stream(urlArr).parallel().forEach(u->{
            if(tkList.isEmpty()){
                throw new NullPointerException("请先添加开发者秘钥");
            }

            String url = u.replace("{tk}", tkList.get(0));
            System.out.println(url);
            String layerName = url.split("tianditu.gov.cn/")[1].split("/wmts?")[0];
            System.out.println(layerName);
            if(layerName.endsWith("c")){
                //等经纬度
                getTk(minZoom, maxZoom, Double.valueOf(startLatf), Double.valueOf(endLatf), Double.valueOf(startLonf), Double.valueOf(endLonf), url, basePath, layerName);
            }else{
                getMk(minZoom, maxZoom, Double.valueOf(startLatf), Double.valueOf(endLatf), Double.valueOf(startLonf), Double.valueOf(endLonf), url, basePath, layerName);
            }
        });
    }

    //获取文件下载流
    public static InputStream getFileInputStream(String url) throws MessageException {
        InputStream is = null;
        CloseableHttpClient httpclient = HttpClients.createDefault();
        HttpGet request = new HttpGet(url);
        request.setHeader("User-Agent", "Mozilla/4.0 (compatible; MSIE 5.0; Windows NT; DigExt)");
        HttpResponse response = null;
        try {
            response = httpclient.execute(request);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        response.setHeader("Content-Type", "application/octet-stream");
        int statusCode = response.getStatusLine().getStatusCode();
        if (statusCode == HttpStatus.SC_OK) {
            HttpEntity entity = response.getEntity();
            try {
                is = entity.getContent();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }else {
            try {
                String body = EntityUtils.toString(response.getEntity());
                System.out.println("请求异常，状态码："+statusCode+" 。 返回消息："+ body);
                // 429 请求数量过多 "message":"API rate limit exceeded"
                if(statusCode == HttpStatus.SC_TOO_MANY_REQUESTS){
                    throw new MessageException("请求异常，状态码："+statusCode+" 。 返回消息："+ body);
                }
            } catch (IOException e) {
//                throw new RuntimeException(e);
            }
        }
        return is;
    }

    public static void FileDownload(String filePath, String newUrl){
        {
            File file = new File(filePath);
            // 图内容小于 1024 Bit，则删除
            if(file.exists() && (isPngCorrupted(file) || file.length() < BAD_IMAGE_SIZE)){
                System.out.println(filePath+"，文件size："+file.length()+", 需要删除重新下载!");
                file.delete();
            }
            if(!file.exists()){
                if(!file.getParentFile().exists()){
                    file.getParentFile().mkdirs();
                }
                boolean loop = true;
                int count = 0;
                long sleepTime = SLEEP_TIME;
                while(loop && count<MAX_LOOP_TIME){//下载出错进行重试，最多5次
                    count++;
                    try {
                        try {
                            Thread.sleep(sleepTime);//主线程休眠1秒，等待线程池运行结束，同时避免一直死循环造成CPU浪费
                        } catch (InterruptedException e) {
                        }
                        InputStream in = getFileInputStream(newUrl);
                        if(in ==null){
                            if(file.exists()) {
                                file.delete();
                                System.out.println("文件下载失败，文件删除：" + filePath);
                            }
                            // 无法读取到文件流，说明文件已经到次数限制了，这里要换key
                            if(!tkList.isEmpty()) {
                                Map<String, String> params = getParams(newUrl);
                                if (params.containsKey("tk") && !"".equals(params.get("tk"))) {
                                    // 过期的Key，要从
                                    String tk = params.get("tk");
                                    tkList.remove(tk);
                                    if(!tkList.isEmpty()){
                                        newUrl = newUrl.replace(tk, tkList.get(0));
                                        // 这里重新循环，不执行后续代码了，重新进行下载
                                        continue;
                                    }
                                }
                            }
                            // 有可能是key当日的使用次数用完，抛出空指针异常，让线程中断
                            throw new NullPointerException("文件下载失败");
                        }
                        OutputStream out = new BufferedOutputStream(new FileOutputStream(file));
                        byte[] b = new byte[8192];
                        int len = 0;
                        while((len = in.read(b)) > -1){
                            out.write(b, 0, len);
                            out.flush();
                        }
                        System.out.println("filePath = "+filePath+"  ; url = "+newUrl);
                        out.close();
                        in.close();
                        loop = false;
                    } catch (MessageException e){
                        loop = true;
                        if(e.getMsg().contains("状态码："+HttpStatus.SC_TOO_MANY_REQUESTS)) {
                            System.out.println("filePath = "+filePath+"，请求过于频繁，退出");
                            if(!tkList.isEmpty()) {
                                tkList.remove(0);
                            }else{
                                loop = false;
                                throw new RuntimeException("filePath = "+filePath+"，请求过于频繁，退出");
                            }
                        }else {
                            // 将睡眠时间延长一倍。
                            sleepTime *= 2;
                            System.out.println("filePath = "+filePath+"，请求过于频繁，线程休眠" + sleepTime + "ms");
                        }
                    } catch (Exception e) {
                        System.out.println("filePath = "+filePath+"  ; url = "+newUrl+"  ; e = "+e.getMessage());
                        loop = false;
                    }
                }
                if(loop){
                    System.out.println("下载失败："+newUrl);
                }
            }else{
                System.out.println("文件已存在："+filePath);
            }
        }
    }


    //等经纬度
    public static void getTk(int minZoom, int maxZoom, final double startLatf, final double endLatf, final double startLonf, final double endLonf, String url, String basePath, String layerName){
        //等经纬度
        IntStream.rangeClosed(minZoom, maxZoom).parallel().forEach(z->{
            double startLat = startLatf;//开始纬度（从北到南）
            double endLat = endLatf;//结束纬度（从北到南）
            double startLon = startLonf;//开始经度（从西到东）
            double endLon = endLonf;//结束经度（从西到东）
            if(z > 10){//天地图10级以上只有中国区域
                startLat = startLatf > startLatChina ? startLatChina : startLatf;//开始纬度（从北到南）
                endLat = endLatf < endLatChina ? endLatChina : endLatf;//结束纬度（从北到南）
                startLon = startLonf < startLonChina ? startLonChina : startLonf;//开始经度（从西到东）
                endLon = endLonf > endLonChina ? endLonChina : endLonf;//结束经度（从西到东）
            }

            double deg = 360.0 / Math.pow(2, z) / 256;//一个像素点代表多少度
            int startX = (int)((startLon + 180) / deg / 256);//减数取整
            int endX = (int)((endLon + 180) / deg / 256);//加数取整
            int startY = (int)((90 - startLat) / deg / 256);
            int endY = (int)((90 - endLat) / deg / 256);
            IntStream.rangeClosed(startY, endY).parallel().forEach(y->{
                IntStream.rangeClosed(startX, endX).parallel().forEach(x->{
                    final String newUrl = url.replace("{server}", servers[(int) (Math.random()*servers.length)]).replace("{z}", z+"").replace("{y}", y+"").replace("{x}", x+"");
                    //System.out.println(newUrl);
                    final String filePath = basePath + "/" + layerName + "/" + z + "/" + x + "/" + y + ".png";
                    FileDownload(filePath, newUrl);
                });
            });
        });
    }

    //墨卡托
    public static void getMk(int minZoom, int maxZoom, final double startLatf, final double endLatf, final double startLonf, final double endLonf, String url, String basePath, String layerName){
        IntStream.rangeClosed(minZoom, maxZoom).parallel().forEach(z->{
            //墨卡托
            double startLat = startLatf;//开始纬度（从北到南）
            double endLat = endLatf;//结束纬度（从北到南）
            double startLon = startLonf;//开始经度（从西到东）
            double endLon = endLonf;//结束经度（从西到东）
            /**
             * earthRadius = 6378137;//地球赤道半径6378137米
             * aa = earthRadius * (Math.PI - 0);//赤道周长的一半，20037508.3427892
             * 85.05112877980659 = (Math.atan(Math.exp(aa / earthRadius))-Math.PI/4)*2 * 180 / Math.PI;//墨卡托最大有效纬度
             */
            if(startLat > 85.051128){
                System.out.println("墨卡托投影起始纬度最大为：85.051128，下载区域将被限定。");
                startLat = 85.051128;
            }
            if(endLat < -85.051128){
                System.out.println("墨卡托投影终止纬度最小为：-85.051128，下载区域将被限定。");
                endLat = -85.051128;
            }

            if(z > 10){//天地图10级以上只有中国区域
                startLat = startLat > startLatChina ? startLatChina : startLat;//开始纬度（从北到南）
                endLat = endLat < endLatChina ? endLatChina : endLat;//结束纬度（从北到南）
                startLon = startLon < startLonChina ? startLonChina : startLon;//开始经度（从西到东）
                endLon = endLon > endLonChina ? endLonChina : endLon;//结束经度（从西到东）
            }
            double deg = 360.0 / Math.pow(2, z) / 256;//一个像素点代表多少度
            int startX = (int)((startLon + 180) / deg / 256);
            int endX = (int)((endLon + 180) / deg / 256);
            /**
             * 这里是基于等经纬度坐标转墨卡托坐标的公式得到的算法。（）
             * 先算出当前地图级别下纬度方向用该有多少个像素点（一个瓦片是256个点，一共有Math.pow(2, z)个瓦片）
             * 再算出指定纬度坐标与赤道的距离（北半球为正南半球为负）（这里抵消了带入赤道周长的计算，因为它是一个固定值，把赤道周长定为1就抵消了）：Math.log(Math.tan((90 + startLat) * Math.PI / 360)) / (Math.PI / 180)
             * 就可以得到纬度坐标距赤道有多少个点：(Math.log(Math.tan((90 + startLat) * Math.PI / 360)) / (Math.PI / 180)) / (360/Math.pow(2, z)/256) + 0.5)
             * 然后就可以计算出其瓦片坐标。
             */
            int startY = (((int)Math.pow(2, z) * 256 / 2) - (int)((Math.log(Math.tan((90 + startLat) * Math.PI / 360)) / (Math.PI / 180)) / (360/Math.pow(2, z)/256) + 0.5)) / 256;
            int endY = (((int)Math.pow(2, z) * 256 / 2) - (int)((Math.log(Math.tan((90 + endLat) * Math.PI / 360)) / (Math.PI / 180)) / (360/Math.pow(2, z)/256) + 0.5)) / 256;
            //加入判断是等经纬度还是墨卡托
            IntStream.rangeClosed(startY, endY).parallel().forEach(y->{
                IntStream.rangeClosed(startX, endX).parallel().forEach(x->{
                    final String newUrl = url.replace("{server}", servers[(int) (Math.random()*servers.length)]).replace("{z}", z+"").replace("{y}", y+"").replace("{x}", x+"");
                    //System.out.println(newUrl);
                    final String filePath = basePath + "/" + layerName + "/" + z + "/" + x + "/" + y + ".png";
//                    System.out.println("filePath = "+filePath+"  ; url = "+newUrl);
                    FileDownload(filePath, newUrl);
                });
            });
        });
    }


    // 获取URL参数, 可以用第三方工具实现，这里不想再加入其他依赖了。
    public static Map<String, String> getParams(String urlString) throws Exception {
        URL url = new URL(urlString);
        String query = url.getQuery();
        Map<String, String> params = new HashMap<>();
        if (query != null) {
            String[] pairs = query.split("&");
            for (String pair : pairs) {
                String[] keyValue = pair.split("=");
                if (keyValue.length > 1) {
                    params.put(URLDecoder.decode(keyValue[0], StandardCharsets.UTF_8), URLDecoder.decode(keyValue[1], StandardCharsets.UTF_8));
                } else {
                    params.put(URLDecoder.decode(keyValue[0], StandardCharsets.UTF_8), "");
                }
            }
        }
        return params;
    }

    /**
     * 判断图片是否损坏
     * @param file
     * @return
     */
    public static boolean isPngCorrupted(File file) {
        // Java标准库支持，简单直接，仅能检测部分损坏（如数据截断、格式错误），对元数据损坏不敏感。
//        try {
//            BufferedImage image = ImageIO.read(file);
//            return image == null; // 返回null表示损坏
//        } catch (IOException e) {
//            return true; // 异常表示损坏
//        }
        // 使用第三方库（Apache Commons Imaging）提供更全面的格式支持与损坏检测
        try {
            Imaging.getImageInfo(file); // 尝试解析图像信息
            return false;
        } catch (IOException e) {
            return true; // 解析失败即损坏
        }
    }
}
